kim.zhang

风在前,无惧!


  • 首页

  • 标签42

  • 分类12

  • 归档94

  • 搜索

shiro.md

发表于 2021-11-27 更新于 2021-12-12
本文字数: 30k 阅读时长 ≈ 28 分钟

文章中讨论的内容基于以下版本的shiro:

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.8.0</version>
</dependency>

参考资料

INI配置文件
自定义PermissionResolver
加密/解密

QuickStart

使用IniSecurityManagerFactory创建SecurityManager,会创建一个DefaultSecurityManager,DefaultSecurityManager里的Realm类是IniRealm.
IniSecurityManagerFactory已过时。可以使用新的方式创建SecurityManager:
1.创建一个DefaultSecurityManager
2.设置DefaultSecurityManager的Realm为IniRealm

1
2
3
DefaultSecurityManager securityManager = new DefaultSecurityManager();
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
SecurityUtils.setSecurityManager(securityManager);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Test
public void hello() {
// 从ini配置文件中获取信息构造SecurityManager,并传递给SecurityUtils
IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = securityManagerFactory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

// 当前登陆的用户,这里的用户信息是保存在shiro.ini文件,Subject会自动绑定到当前线程
Subject subject = SecurityUtils.getSubject();
// isAuthenticated判断当前用户是否登陆
if (!subject.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken("sang", "1234");
try {
// login进行登陆操作,登陆失败会出现不同的异常,根据异常的不同进行处理
subject.login(token);
} catch (UnknownAccountException e) {
log.error("未知账号进行登陆");
} catch (IncorrectCredentialsException e) {
log.error("登陆账号密码错误");
} catch (LockedAccountException e) {
log.error("登陆账号被锁定");
} catch (AuthenticationException e) {
e.printStackTrace();
log.error("登陆失败");
}
}

log.info("当前登陆的用户是:" + subject.getPrincipal());
log.info("是否具有管理员权限:" + subject.hasRole("admin"));
log.info("是否具有管理员的创建权限:" + subject.isPermittedAll("admin:create"));
}

ini文件配置:
1.变量名 = 全限定类名会自动创建一个类实例,相对于调用public无参构造器创建对象
2.变量名.属性=值 自动调用相应的setter方法进行赋值,相当于调用setter方法设置常量值
3.$变量名 引用之前的一个对象实例,相当于调用setter方法设置对象引用

Principals/credentials

在shiro中,用户需要提供principals(身份)和credentials(证明)给shiro,从而应用能验证用户身份:

principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/密码/手机号。
credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。

最常见的principals和credentials组合就是用户名/密码了。

身份认证

1.1 步骤

1.收集用户身份/凭证,比如用户名/密码
2.调用Subject.login进行登陆,如果认证失败,捕获AuthenticationException异常及其子类异样,返回错误信息给用户
3.调用Subject.logout进行退出操作

1.2 流程

1.首先调用Subject.login(token)进行登录,其会自动委托给SecurityManager
2.SecurityManager会委托给Authenticator进行身份验证,Authenticator才是真正的身份验证者,默认使用的Authenticator是ModularRealmAuthenticator,此处可以实现自定义的Authenticator
4.如果有多个Realm,Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证
5.Authenticator会把相应的token传入AuthenticatingRealm,最终调用AuthenticatingRealm类的getAuthenticationInfo方法进行身份|密码验证。

AuthenticatingRealm类的getAuthenticationInfo方法中有一个doGetAuthenticationInfo方法,它是完成身份认证的方法。自定义的Realm类可以重写这个方法进行自定义的身份认证。完成身份认证后,doGetAuthenticationInfo方法返回AuthenticationInfo(比如SimpleAuthenticationInfo),AuthenticationInfo返回的信息包含了如何校验密码的信息,AuthenticatingRealm会调用assertCredentialsMatch(token,info)来完成密码校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
// 根据不同的Realm实现类,完成身份校验,同时返回AuthenticationInfo
info = doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}

if (info != null) {
// 根据AuthenticationInfo和token中的信息进行校验,完成密码验证
assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}

return info;
}

Realm

SecurityManager是管理认证和授权的,一个SecurityManager可以对应多个Realm类来实现认证和授权。
SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法,也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作。
在Shiro中,内置了几种Reaml类。默认使用的是IniRealm。
比如,在使用账号密码进行登陆的时候,会进入到AuthenticatingRealm类中,最终调用Realm类中的doGetAuthenticationInfo方法。

JdbcRealm

可以使用Shiro内置的JdbcRealm类到数据中查询,JdbcRealm中内置了sql语句,从sql语句中可以推出表的结构。
users表: 由username,password,password_salt字段组成。

如果需要使用JdbcRealm类:
1.需要导入数据库和表
2.需要配置DataSource和数据库连接信息

1
2
3
4
5
6
7
8
9
10
dataSource = com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName = com.mysql.cj.jdbc.Driver
dataSource.url = jdbc:mysql://localhost:3306/shirodemo
dataSource.username = root
dataSource.password = root

jdbcRealm = org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.dataSource = $dataSource
jdbcRealm.permissionsLookupEnabled = true
securityManager.realms = $jdbcRealm

可以配置多个Realm类,使用逗号分隔

1
securityManager.realms = $jdbcRealm,$myRealm

自定义Realm类

报错:Ini instance and/or resourcePath resulted in null or empty Ini configuration. Cannot load account data.
在写测试类的时候,可能加入了@SpringbootTest,加了这个注解后springboot内置的SecurityManager就生效了,可能与自定义的IniSecurityManager冲突了。

自定义Realm类有以下方式:

1.1 实现Realm接口

实现接口的getAuthenticationInfo方法,需要自己实现密码的校验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyRealm implements Realm {
@Override
public String getName() {
return "MyRealm";
}

@Override
public boolean supports(AuthenticationToken authenticationToken) {
// 支持的token类型
return authenticationToken instanceof UsernamePasswordToken;
}

// 登陆逻辑校验,这里抛出的异常将在Subject.login那里捕获到
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = authenticationToken.getPrincipal().toString();
String password = new String((char[]) authenticationToken.getCredentials());
if (!"admin".equals(username)) {
throw new UnknownAccountException("账号不存在");
}
if (!"admin".equals(password)) {
throw new IncorrectCredentialsException("密码不正确");
}
return new SimpleAuthenticationInfo(username, password, getName());
}
}

在ini配置文件中注入自定义的Realm类

1
2
3
[main]
MyRealm = kim.zhang.springbootshirodemo.shiro.MyRealm
securityManager.realms = $MyRealm
1.2 继承AuthenticatingRealm

AuthenticatingRealm重写了getAuthenticationInfo方法,它会调用我们自定义实现的Realm类的doGetAuthenticationInfo方法来获取AuthenticationInfo信息。
AuthenticationInfo信息包括密码,加密盐值等。
之后AuthenticationRealm会调用assertCredentialsMatch来校验密码。
AuthenticatingRealm继承了CachingRealm,也具有缓存的功能。

与1.1中实现Realm的接口相比,继承AuthenticatingRealm接口无需我们去校验密码,我们只需要把密码,加密盐值等信息封装到AuthenticationInfo中即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
if (info == null) {
// 调用我们自己自定义Realm实现的方法
info = this.doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
this.cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}

if (info != null) {
// 校验密码
this.assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}

return info;
}

实现自定义的Realm类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyRealm extends AuthenticatingRealm {
@Override
public String getName() {
return "MyRealm";
}

@Override
public boolean supports(AuthenticationToken authenticationToken) {
// 支持的token类型
return authenticationToken instanceof UsernamePasswordToken;
}

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();
if (!"sang".equals(username)) {
throw new UnknownAccountException("未知账号");
}
// 从数据库中查询加密之后的密码
String dbPassword = "2037e1876a21e65b4606215d773e74ec74ed827291e4ce4372f6be9a915ae6e3096c2b8a323588aa083015fe4ee5ac201ba5cff118337947ac3911c90c954ea6";
// 第三个参数是盐值,传入盐值与数据库加密后的密码进行校验
return new SimpleAuthenticationInfo(username, dbPassword, ByteSource.Util.bytes(username), getName());
}
}

ini配置文件中配置使用的Realm类以及密码加密:

1
2
3
4
5
6
7
8
# 密码加密
sha512 = org.apache.shiro.authc.credential.Sha512CredentialsMatcher
# 迭代次数
sha512.hashIterations = 1024
# 自定义的Realm
myRealm = kim.zhang.springbootshirodemo.shiro.MyRealm
myRealm.credentialsMatcher = $sha512
securityManager.realms = $myRealm
1.3 继承AuthorizingRealm

AuthenticatingRealm类不是Authorizer的实例,无法管理角色和权限,但是能管理登陆认证。
AuthorizatingRealm类是Authorizer的实例,在AuthenticatingRealm的基础上,可以管理角色和权限。
AuthorizatingRealm类具有缓存、身份认证、授权管理的功能,一般实现自定义的Realm类继承AuthorizingRealm类是比较合适的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class MyRealm extends AuthorizingRealm {
@Override
public String getName() {
return "MyRealm";
}

@Override
public boolean supports(AuthenticationToken authenticationToken) {
// 支持的token类型
return authenticationToken instanceof UsernamePasswordToken;
}

// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();
// 密码以char[]存储
String password = new String((char[]) authenticationToken.getCredentials());
if (!"sang".equals(username)) {
throw new UnknownAccountException("未知账号");
}
// 对密码加盐处理,这里使用username作为盐值
Sha512Hash dbPassword = new Sha512Hash(password, username, 1024);
// 第三个参数是盐值
return new SimpleAuthenticationInfo(username, dbPassword, ByteSource.Util.bytes(username), getName());
}


// 角色,权限
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
HashSet<String> roles = new HashSet<>();
HashSet<String> permissions = new HashSet<>();
String username = principalCollection.getPrimaryPrincipal().toString();
if ("sang".equals(username)) {
roles.add("普通用户");
permissions.add("book:update");
}
// 设置角色
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(roles);
// 设置权限
simpleAuthorizationInfo.setStringPermissions(permissions);
return simpleAuthorizationInfo;
}
}

hasRole,isPermitte最终都是调用的Realm的doGetAuthorizationInfo来进行角色、权限的校验。

AuthenticationStrategy

如果配置了多个Realm类,有3种不同的认证策略:
1.AllSuccessfulStrategy: 所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了
2.AtLeastOneSuccessfulStrategy: 只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,返回所有Realm身份验证成功的认证信息
3.FirstSuccessfulStrategy: 只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略;

1
2
3
4
5
authenticator = org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator = $authenticator
allSuccessfulStrategy = org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy = $allSuccessfulStrategy
securityManager.realms = $jdbcRealm,$myRealm

如果报错No realms have been configured! One or more realms must be present,则要把认证策略的配置优先于Realm类的配置。

授权

1.1 步骤

调用hasRole/isPermitted方法检查是否具有对应的角色/权限,执行不同的操作。

hasRole/hasAllRole: 判断用户是否拥有某个/所有角色,返回布尔值,不会抛出异常
checkRole/checkRoles: 判断用户是否拥有某个/所有角色,判断为假的时候会抛出UnauthorizationException

isPermitted/isPermittedAll: 判断用户是否拥有某个/所有权限,返回布尔值,不会抛出异常
checkPermission/checkPermissions: 判断用户是否拥有某个/所有权限,判断为假的时候会抛出UnauthorizationException

1.2 流程

1.首先调用Subject.isPermitted/hasRole*接口,其会委托给SecurityManager,而SecurityManager接着会委托给Authorizer
2.Authorizer是真正的授权者,默认的Authorizer是ModularRealmAuthorizer,如果我们调用如isPermitted(“user:view”),其首先会通过PermissionResolver(默认是WildcardPermissionResolver)把字符串转换成相应的Permission实例。如果我们调用如isRole(“user”),则不会解析成Permission实例,直接使用字符串进行判断。
3.在进行授权之前,其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限
4.Authorizer会判断Realm的角色/权限是否和传入的匹配,如果有多个Realm,会进行循环判断,如果匹配如isPermitted
/hasRole*会返回true,否则返回false表示授权失败。

ModularRealmAuthorizer#hasRole:

1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
assertRealmsConfigured();
for (Realm realm : getRealms()) {
// 首先检查相应的Realm是否实现了实现了Authorizer
if (!(realm instanceof Authorizer)) continue;
// 如果实现了Authorizer,那么接着调用其相应的isPermitted*/hasRole*接口进行匹配
// 如果有一个Realm匹配那么将返回true,否则返回false
if (((Authorizer) realm).hasRole(principals, roleIdentifier)) {
return true;
}
}
return false;
}

密码加密

1.1 Shiro提供的工具

Shiro提供了base64和16进制字符串编码/解码的API支持,方便一些编码解码操作。Shiro内部的一些数据的存储/表示都使用了base64和16进制字符串。

Base64

1
2
3
4
5
6
7
// 加密
byte[] encode = Base64.encode("hello".getBytes(StandardCharsets.UTF_8));
System.out.println("encode:" + CodecSupport.toString(encode));

// 解密
String decodeString = Base64.decodeToString(encode);
System.out.println("decode:" + decodeString);

Hex

1
2
3
4
5
String encodeToString = Hex.encodeToString("hello".getBytes(StandardCharsets.UTF_8));
System.out.println("encode:" + encodeToString);

byte[] decode = Hex.decode(encodeToString);
System.out.println("decode:" + CodecSupport.toString(decode));

SimpleHash

1
2
3
Md5Hash md5Hash = new Md5Hash("1234", "salt", 2);
SimpleHash simpleMd5Hash = new SimpleHash("MD5", "1234", "salt", 2);
Sha512Hash sha512Hash = new Sha512Hash("1234", "salt", 2);

DefaultHashService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 DefaultHashService defaultHashService = new DefaultHashService();
// defaultHashService.setHashAlgorithmName("MD5");
// defaultHashService.setHashIterations(2);

HashRequest hashRequest = new HashRequest.Builder().setAlgorithmName("MD5")
.setIterations(2)
.setSalt(ByteSource.Util.bytes("salt"))
.setSource(ByteSource.Util.bytes("1234"))
.build();
Hash hash = defaultHashService.computeHash(hashRequest);
// cc8df586d1d3e66687e798016b507ce3
System.out.println(hash);
// zI31htHT5maH55gBa1B84w==
System.out.println(hash.toBase64());

使用SHA-512加密时Ini文件的配置:

1
2
3
4
5
sha512 = org.apache.shiro.authc.credential.Sha512CredentialsMatcher
# 迭代次数
sha512.hashIterations = 1024
# 修改JdbcRealm中的credentialsMatcher属性
jdbcRealm.credentialsMatcher = $sha512
1.2 PasswordMatcher

Shiro提供了PasswordService及CredentialsMatcher用于提供加密密码及验证密码服务,不能提供自己的盐。

使用DefaultPasswordService配合PasswordMatcher实现简单的密码加密与验证服务。
在保存密码到数据库时,使用PasswordService加密密码。在Realm类设置PasswordMatcher来验证密码服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Bean
public MyRealm userRealm() {
MyRealm myRealm = new MyRealm();
PasswordMatcher passwordMatcher = new PasswordMatcher();
passwordMatcher.setPasswordService(passwordService());
myRealm.setCredentialsMatcher(passwordMatcher);
return myRealm;
}

@Bean
public PasswordService passwordService() {
DefaultPasswordService defaultPasswordService = new DefaultPasswordService();
DefaultHashService hashService = new DefaultHashService();
hashService.setHashIterations(1024);
# 可以设置不同的加密算法, 默认是SHA-512
hashService.setHashAlgorithmName("SHA-512");
defaultPasswordService.setHashService(hashService);
# 设置生成密码的格式,例如$shiro1$MD5$1024$$oguOaCpy7qwASYR4Vc7Lhg==
defaultPasswordService.setHashFormat(new Shiro1CryptFormat());
defaultPasswordService.setHashFormatFactory(new DefaultHashFormatFactory());
return defaultPasswordService;
}
1.3 HashedCredentialsMatcher

和之前的PasswordMatcher不同的是,它只用于密码验证,且可以提供自己的盐,而不是随机生成盐。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Bean
public MyRealm userRealm() {
MyRealm myRealm = new MyRealm();
myRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myRealm;
}

@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("SHA-512");
hashedCredentialsMatcher.setHashIterations(1024);
return hashedCredentialsMatcher;
}

密码加盐

如果两个密码原文一样,生成的密文也是一样的。我们可以使用密码加盐的方式来处理。

如果使用的是JdbcRealm类,JdbcRealm提供了4种SaltType:
SaltStyle 含义
NO_SALT 默认,密码不加盐
CRYPT 密码是以Unix加密方式储存的
COLUMN salt是单独的一列储存在数据库中
EXTERNAL salt没有储存在数据库中,需要通过JdbcRealm.getSaltForUser(String)函数获取

在COLUMN这种情况下,SQL查询结果应该包含两列,第一列是密码,第二列是盐,严格区分顺序。
在JdbcRealm类中默认是使用字段password_salt来指定盐值的。我们也可以在ini文件中自定义

1
2
3
4
5
6
7
8
# 加盐的类型
jdbcRealm.saltStyle = COLUMN
# 自定义盐的字段,这里是严格区分顺序的,username作为盐值
jdbcRealm.authenticationQuery = select password,username from users where username=?
# 盐值是否编码,默认是true,则保存密码到数据库时也需要编码
jdbcRealm.saltIsBase64Encoded = false
# 修改JdbcRealm中的credentialsMatcher属性
jdbcRealm.credentialsMatcher = $sha512

由于ini文件中不支持枚举类型,而SaltType的值是枚举类型的,我们需要一个转换器将String类型的值转换成枚举类型,绑定到JdbcRealm.SaltType上。

1
2
3
4
5
6
7
8
9
10
11
12
 // 将ini文件中的字符串配置值转换成SaltStyle的枚举类
BeanUtilsBean.getInstance().getConvertUtils().register(new AbstractConverter() {
@Override
protected <T> T convertToType(Class<T> aClass, Object o) throws Throwable {
return aClass.cast(o.toString());
}

@Override
protected Class<?> getDefaultType() {
return null;
}
}, JdbcRealm.SaltStyle.class);

springboot启动项目报错

Description:

Parameter 0 of method authorizationAttributeSourceAdvisor in org.apache.shiro.spring.boot.autoconfigure.ShiroAnnotationProcessorAutoConfiguration required a bean named ‘authenticator’ that could not be found.

Action:

Consider defining a bean named ‘authenticator’ in your configuration.

Springboot会自动注入一个DefaultWebSecurityManager.
而SecurityManger的创建过程需要依赖Authorizer.默认的情况下,如果我们不提供Authorizer,springboot是会自动创建这个Authorizer的bean的.
但是很巧的是,我们自己实现的Realm类需要继承AuthorizingRealm类,而AuthorizingRealm类实现了Authorizer接口

1
public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware {

当springboot在创建SecurityManager时需要一个Authorizer,而一旦我们自己提供了Authorizer这个bean(Realm类继承了AuthorizingRealm类),springboot就不会再创建这个Authorizer了。
springboot在创建SecurityManager是通过代理去创建的,会去查找bean的名称叫做authorizer的bean,如果找不到,会出现了以上的错误。

解决办法:
1.将自定义的Realm类的bean名称设置为authorizer
2.在配置类中添加authorizer

1
2
3
4
5
// bean的名称一定要叫authorizer
@Bean
public Authorizer authorizer() {
return new ModularRealmAuthorizer();
}

3.不使用springboot自动配置注入的SecurityManager,自己注入一个SecurityManager

‘authenticator’ that could not be found

登陆页面

1.如果登陆页面放在template目录下,需要引入thymeleaf依赖。
如果页面是.html结尾的,后端控制器返回页面的时候可以带后缀,也可以不带后缀。

1
2
3
4
5
6
7
8
9
@Controller
public class PageController {

@GetMapping("/login")
public String login() {
// 也可以写成login.html
return "login";
}
}

2.Shiro默认的登陆页面是login.jsp
如果指定了shiro.loginUrl之后,无需再对loginUrl配置anon的过滤器。

1
2
shiro:
loginUrl: /login

如果后端跳转前端页面的url是login(GET方法),后端真正实现登陆逻辑的url是login(POST方法),
则在ShiroFilterChainDefinition配置的/login指的是POST方法的/login。

1
defaultShiroFilterChainDefinition.addPathDefinition("/login", "authc");

3.前端使用Form表单的会导致进行两次认证请求。可以将input改成button,点击按钮的时候再提交表单。

1
2
3
4
5
6
7
8
9
<button id="loginBtn">登陆系统</button>

<script type="application/javascript">
$(function () {
$("#loginBtn").onclick(function () {
$("#loginForm").submit()
})
})
</script>

登出系统

如果不需要实现自定义的登出逻辑,直接使用Shrio提供的logout过滤器,会清除缓存后重定向到登陆页面。
可以查看LogoutFilter查看具体的细节。

1
defaultShiroFilterChainDefinition.addPathDefinition("/logout", "logout");

如果需要自定义登出逻辑,自定义一个/logout的url实现登出逻辑。
如果配置了/logout是anon,在退出登陆的时候需要判断isAuthenticated()
如果配置/logout是authc,在退出登陆的时候无需判断isAuthenticated()

1
2
3
4
5
6
7
8
@GetMapping("/logout")
@ResponseBody
public String logout() {
Subject subject = SecurityUtils.getSubject();
// 如果没有配置/logout是anon,执行这行代码的时候用户一定是已经登陆的状态了,不再需要subject.isAuthenticated()的判断
subject.logout();
return "退出系统成功";
}

缓存管理

Shiro CacheManager
Shiro Caching官方文档

Shiro提供了类似于Spring的Cache 抽象,即Shiro本身不实现Cache,但是对Cache进行了又抽象,方便更换不同的底层Cache实现.

CacheManager是Shiro包中的一个接口,任意的数据源只要实现了这个接口,都可以嵌入到Shiro中.
CacheManager是一个容器,管理着Cache<K,V>,根据cacheName获取Cache<K,V>
Cache接口也是Shiro中的一个接口. Cache<K,V>是一个小容器.
在Shiro中,AuthenticationInfo、AuthorizationInfo会各自生成一个Cache,根据唯一的名字cacheName保存到CacheManager这个容器中.

1
2
3
public interface CacheManager {
<K, V> Cache<K, V> getCache(String cacheName) throws CacheException;
}

如果在SecurityManager中设置了CacheManager,SecurityManager会自动检测相应的对象(如Realm)是否实现了CacheManagerAware接口并自动注入对应的CacheManager.
CachingRealm实现了CacheManagerAware接口,而AuthentingRealm | AuthorizatingRealm 都直接|间接实现了CacheManager.那么实现了CacheManagerAware的接口的Realm都会被设置这个CacheManager.

1.1 MemoryConstrainedCacheManager

MemoryConstrainedCacheManager是基于Map数据结构的缓存,它只适用于单个JVM的情况,而不适用于分布式的环境。
每个cacheName都对应着一个MapCache实例。

给SecurityManager添加一个CacheManager,同时开启Realm类的AuthenticationCache/AuthorizationCache.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(userRealm());
// 开启缓存
defaultWebSecurityManager.setCacheManager(cacheManager());
return defaultWebSecurityManager;
}

@Bean
public CacheManager cacheManager() {
return new MemoryConstrainedCacheManager();
}

@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
// Realm开启缓存
userRealm.setCachingEnabled(true);
// 开启身份认证缓存
userRealm.setAuthenticationCachingEnabled(true);
// 设置身份认证缓存的名称
userRealm.setAuthenticationCacheName("authenticationCache");
// 开启授权缓存
userRealm.setAuthorizationCachingEnabled(true);
// 设置授权缓存的名称
userRealm.setAuthenticationCacheName("authorizationCache");
userRealm.setCredentialsMatcher(credentialsMatcher());
return userRealm;
}
1.2 EhCacheManager

使用EhCache需要导入相应的依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.7.1</version>
</dependency>

修改CacheManager为EhCacheManager实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(userRealm());
// 开启缓存
defaultWebSecurityManager.setCacheManager(cacheManager());
return defaultWebSecurityManager;
}

@Bean
public CacheManager cacheManager() {
return new EhCacheManager();
}

@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
// Realm开启缓存
userRealm.setCachingEnabled(true);
// 开启身份认证缓存
userRealm.setAuthenticationCachingEnabled(true);
// 设置身份认证缓存的名称
userRealm.setAuthenticationCacheName("authenticationCache");
// 开启授权缓存
userRealm.setAuthorizationCachingEnabled(true);
// 设置授权缓存的名称
userRealm.setAuthenticationCacheName("authorizationCache");
userRealm.setCredentialsMatcher(credentialsMatcher());
return userRealm;
}

JWT + Shiro

JWT

JWT的格式: header(Base64) + payload(Base64) + siguature(加密算法)

header : 声明JWT的类型以及加密的算法
payload: 用户自定义的信息
siguature: Base64编码后的header + . + Base64编码后的payload进行加密

Base64可以被encode,相当于是明文,不能存储重要的信息,比如不能在payload中存储密码

JWT的验证: JWT的验证只是和加密的算法和使用的secert有关,与payload的自定义信息无关.

自定义Shiro过滤器

Shiro + JWT

思路:
在用户使用login登陆的时候,不使用Shiro去进行身份认证.自定义方法验证用户名密码正确后,使用JWT给已经认证的用户颁发一个token.
当调用其他需要登陆后才能使用的接口时,使用颁发的token来请求其他的接口.
接口会通过自定义的Shiro Filter,在Filter中从request | request header中取出token,并使用getSubject.login方法来校验token并进行身份认证.
当token身份认证成功之后,如果是需要访问roles/permissions相关的接口时,会调用Realm的授权方法.

步骤:
1.实现自定义的AuthenticationToken,Realm进行身份认证时使用
2.实现自定义的Realm,对token进行校验和授权
3.实现自定义的Shiro Filter,拦截需要token的请求
4.关闭Shiro的session.JWT是无状态的,由于接入cas会和shiro的session管理冲突,所以关闭shiro的session,进行无状态管理.

1.实现自定义的AuthenticationToken,Realm中doGetAuthenticationInfo方法的参数
Credenticals可以传递空字符串,后面进行身份认证的时候返回SimpleAuthentincationInfo中的Credenticals也返回空字符串就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@AllArgsConstructor
public class JWTToken implements AuthenticationToken {

private String token;

@Override
public Object getPrincipal() {
return token;
}

@Override
public Object getCredentials() {
return token;
}
}

2.实现自定义的Realm
注意一定要实现supports方法,Realm在进行身份认证的时候,如果不支持的support token是不会进行身份认证的.
doGetAuthenticationInfo的返回值SimpleAuthenticationInfo传递的Credentical会与token中的Credientical进行比较
在选择CredentialsMatcher时需要注意

doGetAuthorizationInfo的参数PrincipalCollection的值就是在doGetAuthenticationInfo的返回值SimpleAuthenticationInfo传递的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class JWTRealm extends AuthorizingRealm {

@Autowired
private UserDao userDao;

@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 这里获取的token就是Filter中传递过来的JWTToken
String jwtToken = (String) token.getCredentials();
// 校验token
boolean verify = JWTUtil.verify(jwtToken);
if (!verify) {
throw new IncorrectCredentialsException("无效的token");
}
// 取出token中自定义的username
String username = JWTUtil.getStringClaim(jwtToken, "username");
User user = userDao.findUserByUsername(username);
if (user == null) {
throw new UnknownAccountException("无效的用户");
}
// 默认使用SimpleCredentialsMatcher进行equal的比较,这里的jwtToken与AuthenticationToken进行比较
return new SimpleAuthenticationInfo(username, jwtToken, getName());
}

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
User user = userDao.findUserByUsername(username);
List<Role> roleList = user.getRoles();
Set<String> permissions = new HashSet<>();
for (Role role : roleList) {
Set<String> permissionSet = role.getPermissions().stream().map(Permission::getPermission).collect(Collectors.toSet());
permissions.addAll(permissionSet);
}
Set<String> roles = roleList.stream().map(Role::getRoleName).collect(Collectors.toSet());
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(roles);
simpleAuthorizationInfo.setStringPermissions(permissions);
return simpleAuthorizationInfo;
}
}

3.实现自定义的Shiro Filter
在Filter中进行请求的拦截并调用Subject.login方法委托给Realm进行身份认证和授权.
在Realm中抛出的异常可以在Subject.login处捕获,不能在全局异常处理中捕获.
Filter返回JSON的结果,使用Response用流输出到浏览器.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class JWTFilter extends AccessControlFilter {


@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
// 请求方法是OPTIONS的请求通过,其他请求全部拦截,被拦截的方法会调用onAccessDenied方法
HttpServletRequest request = (HttpServletRequest) servletRequest;
return request.getMethod().equals(RequestMethod.OPTIONS.name());
}

@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String token = request.getHeader("token");
if (token == null) {
this.responseError(request, response, "请先登陆系统");
return false;
}
// 调用Realm类的doGetAuthenticationInfo进行token的校验
try {
getSubject(request, response).login(new JWTToken(token));
} catch (AuthenticationException e) {
// 在Realm类中抛出的异常在这里捕获处理,全局异常处理捕获不到
this.responseError(request, response, e.getMessage());
}
return true;
}

private void responseError(HttpServletRequest request, HttpServletResponse response, String message) throws
IOException {
String origin = request.getHeader("origin");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
// 增加跨域支持
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with, X-Access-Token, datasource-Key");
response.setHeader("Access-Control-Allow-Origin", origin);
response.setStatus(401);
PrintWriter writer = response.getWriter();
writer.write(message);
writer.flush();
writer.close();
}
}

4.Shiro Config配置过滤器,session

session的关闭有以下几个步骤:
1.实现自定义的SubjectFactory,关闭session的创建
2.实现自定义的sessionManager,关闭session轮询校验
3.禁用session持久化

SubjectFactory:

1
2
3
4
5
6
7
8
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
// 不创建session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}

ShiroConfig:
这里的JwtRealm使用的CredenticalsMatcher是默认的SimpleCredenticatlsMatcher,它会将SimpleAuthenticationInfo的Credentical与doGetAuthenticationInfo方法的参数AuthenticaionInfo进行equals的比较.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@Configuration
public class ShiroConfig {


@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager());

// 自定义Filter,Filter名称为token
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("token", new JWTFilter());
shiroFilterFactoryBean.setFilters(filterMap);

Map<String, String> definitionMap = new LinkedHashMap<>();
definitionMap.put("/logout", "logout");
definitionMap.put("/login", "anon");
// 所有请求都经过token filter
definitionMap.put("/**", "token");
shiroFilterFactoryBean.setFilterChainDefinitionMap(definitionMap);
return shiroFilterFactoryBean;
}

@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 禁用Session持久化
DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
defaultSubjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
defaultWebSecurityManager.setSubjectDAO(defaultSubjectDAO);
// 实现自定义的SubjectFactory,不再创建session
defaultWebSecurityManager.setSubjectFactory(statelessDefaultSubjectFactory());
// 实现自定义的sessionManager,关闭session校验轮询
defaultWebSecurityManager.setSessionManager(sessionManager());
defaultWebSecurityManager.setRealm(jwtRealm());
return defaultWebSecurityManager;
}

@Bean
public SessionManager sessionManager() {
DefaultSessionManager defaultSessionManager = new DefaultSessionManager();
// 关闭session校验轮询
defaultSessionManager.setSessionValidationSchedulerEnabled(false);
return defaultSessionManager;
}

@Bean
public StatelessDefaultSubjectFactory statelessDefaultSubjectFactory() {
return new StatelessDefaultSubjectFactory();
}

@Bean
public JWTRealm jwtRealm() {
JWTRealm jwtRealm = new JWTRealm();
jwtRealm.setCacheManager(cacheManager());
jwtRealm.setCachingEnabled(true);
jwtRealm.setAuthenticationCachingEnabled(true);
jwtRealm.setAuthenticationCacheName("authenticationCache");
jwtRealm.setAuthorizationCachingEnabled(true);
jwtRealm.setAuthorizationCacheName("authorizationCache");
return jwtRealm;
}

@Bean
public CacheManager cacheManager() {
return new MemoryConstrainedCacheManager();
}
}

5.JWT的工具类

使用@ConfigurationProperties从配置文件中注入成员属性.
静态成员属性的注入使用非static的setter方法注入.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Component
@Slf4j
@ConfigurationProperties(prefix = "jwt")
public class JWTUtil {

private static String secret;
/**
* 过期时间,单位分钟
*/
private static Long expireTime;

public static String createToken(String username) {
return JWT.create()
.withClaim("username", username)
.withExpiresAt(new Date(System.currentTimeMillis() + (expireTime * 60 * 1000)))
.sign(Algorithm.HMAC512(secret));
}

public static boolean verify(String token) {
try {
JWT.require(Algorithm.HMAC512(secret))
.build()
.verify(token);
} catch (JWTVerificationException e) {
log.warn("token[{}] verify exception.", token);
return false;
}
return true;
}

public static String getStringClaim(String token, String claim) {
DecodedJWT decodedJWT = JWT.decode(token);
return decodedJWT.getClaim(claim).asString();
}

public void setSecret(String secret) {
JWTUtil.secret = secret;
}

public void setExpireTime(Long expireTime) {
JWTUtil.expireTime = expireTime;
}
}

application.yaml:

1
2
3
4
jwt:
secret: "kim"
# 时间单位为分钟
expireTime: 5
一毛也是爱~
Kim.Zhang 微信支付

微信支付

git
  • 文章目录
  • 站点概览
Kim.Zhang

Kim.Zhang

且行且珍惜
94 日志
12 分类
42 标签
E-Mail Weibo
  1. 1. 参考资料
  2. 2. QuickStart
  3. 3. Principals/credentials
  4. 4. 身份认证
    1. 4.1. 1.1 步骤
    2. 4.2. 1.2 流程
  5. 5. Realm
    1. 5.1. JdbcRealm
  6. 6. 自定义Realm类
    1. 6.1. 1.1 实现Realm接口
    2. 6.2. 1.2 继承AuthenticatingRealm
    3. 6.3. 1.3 继承AuthorizingRealm
  7. 7. AuthenticationStrategy
  8. 8. 授权
    1. 8.1. 1.1 步骤
    2. 8.2. 1.2 流程
  9. 9. 密码加密
    1. 9.1. 1.1 Shiro提供的工具
    2. 9.2. 1.2 PasswordMatcher
    3. 9.3. 1.3 HashedCredentialsMatcher
  10. 10. 密码加盐
  11. 11. springboot启动项目报错
  12. 12. 登陆页面
  13. 13. 登出系统
  14. 14. 缓存管理
    1. 14.1. 1.1 MemoryConstrainedCacheManager
    2. 14.2. 1.2 EhCacheManager
  15. 15. JWT + Shiro
    1. 15.1. JWT
    2. 15.2. 自定义Shiro过滤器
粵ICP备19091267号 © 2019 – 2022 Kim.Zhang | 629k | 9:32
本站总访问量 4 次 | 有 309 人看我的博客啦 |
博客全站共176.7k字
载入天数...载入时分秒...
0%